package core;

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Map.Entry;

import agent.AgentLoader;
import agent.DominoAgent;
import core.basic.DominoSet;
import core.basic.Movement;
import core.basic.Piece;
import core.statistics.NStatistics;
import exception.InvalidAgentClassException;
import exception.PropertiesFileError;

public class DominoServer {

	private static final String RESERVED_PROPERTIES_PREFIX = "domino.";
	private static final String ROUNDS_PROPERTY_SUFIX = "rounds";
	public static final String DEFAULT_PROPERTIES_FILE_PATH = "domino.properties";

	private Board gameBoard;
	Map<String, String> agentClasses;
	private ArrayList<AgentInfo> agents;
	private Properties props;

	private int rounds = 0;

	private NStatistics statistics = null;

	public int getRounds() {
		return rounds;
	}

	private static DominoServer instance;

	public static DominoServer getInstance() {
		if (instance == null) {
			try {
				instance = new DominoServer(DEFAULT_PROPERTIES_FILE_PATH);
			} catch (InvalidAgentClassException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (PropertiesFileError e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		return instance;
	}

	public DominoServer(String propertiesFilePath)
			throws InvalidAgentClassException, PropertiesFileError {
		agentClasses = readPropertiesFile(propertiesFilePath);
		loadAgents();
		try {
			rounds = Integer.parseInt((String) props
					.getProperty(RESERVED_PROPERTIES_PREFIX
							+ ROUNDS_PROPERTY_SUFIX));

		} catch (Exception e) {
			throw new PropertiesFileError(
					"Number of rounds property is not correctly set.", e);
		}
	}

	public void loadAgents() {
		try {
			agents = AgentLoader.loadAgents(agentClasses);
		} catch (InvalidAgentClassException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	public void addAgent(AgentInfo a) {
		agents.add(a);
	}

	public void start() {
		for (int i = 0; i < rounds; i++) {
			clearAll();
			prepare();
			showHands();
			gameLoop(i);
		}

	}

	private void showHands() {
		for (AgentInfo info : agents) {
			System.out.println(info.getName() + ":");
			System.out.println(info.getHand());
			System.out.println();
		}
	}

	private Map<String, String> readPropertiesFile(String propertiesFilePath)
			throws PropertiesFileError {
		Properties props = new Properties();
		try {
			props.load(new FileReader(propertiesFilePath));
		} catch (FileNotFoundException e) {
			throw new PropertiesFileError("Properties file: \""
					+ propertiesFilePath + "\" not found.", e);
		} catch (IOException e) {
			throw new PropertiesFileError(
					"Error while reading properties file: \""
							+ propertiesFilePath + "\".", e);
		}

		Map<String, String> agentClasses = new LinkedHashMap<String, String>();

		for (Entry<Object, Object> entry : props.entrySet()) {
			String key = (String) entry.getKey();
			String value = (String) entry.getValue();
			if (!key.startsWith(RESERVED_PROPERTIES_PREFIX)) {
				agentClasses.put(key, value);
			}
		}

		for (String agentName : agentClasses.keySet()) {
			props.remove(agentName);
		}

		this.props = props;

		return agentClasses;
	}

	private void gameLoop(int currentRound) {
		AgentInfo winner = null;
		while (gameIsOn() && (winner == null)) {
			winner = round();
		}
		endGame(winner, currentRound);
	}

	// add prints
	private void endGame(AgentInfo winner, int currentRound) {
		System.out.println("The game has ended");
		showHands();
		if (winner == null) {// não houve vencedor direto, o jogo travou
			winner = getLockWinner();// acha quem tem menos pontos na mão
		}
		if (winner == null) {// houve empate de pontos
			System.out.println("Tie");
		} else {
			int p = getVictoryPoints(winner);
			winner.getStatistics().addPointsCount(p);
			winner.getStatistics().increaseVictoryCount();
			System.out.println(winner.getName() + " has won the game");
		}

		// aqui deve criar/atualizar as estatisticas de acordo com os AgentInfo
		// this.agents
		this.statistics = null;
		this.statistics = new NStatistics(this.agents);
		this.statistics.setCurrentRound(currentRound++);// ++ pois o for que
														// envia o currentRound
														// varia de 0 ateh ...
		// aqui deve printar as percentagens de {vitoria, derrota, pontos
		// ganhos, vezes que passou, tempo pensando}.. para todos os agents
		// disponiveis
		this.statistics.printAllPercentages();

		// System.out
		// .println("/////////////////////////////////////////////////////////////////////////////////////");
		// System.out.println();
	}

	private int getVictoryPoints(AgentInfo winner) {
		int points = 0;
		for (AgentInfo a : agents) {
			if (!a.equals(winner)) {
				points += a.getHand().sumPoints();
			}
		}
		return points;
	}

	private AgentInfo getLockWinner() {
		int points[] = new int[agents.size()];

		int min = 1000000;
		int candidate = 0;

		// acha quem fez menos pontos
		for (int i = 0; i < points.length; i++) {
			points[i] = agents.get(i).getHand().sumPoints();
			min = min > points[i] ? points[i] : min;
			if (min == points[i])
				candidate = i;
		}
		// verifica se houve empate de pontos
		for (int i = 0; i < points.length; i++) {
			if (i != candidate && points[i] == min)
				return null;
		}
		return agents.get(candidate);
	}

	// add prints
	private AgentInfo round() {
		for (AgentInfo info : agents) {
			DominoAgent inTurn = info.getAgent();
			Hand inTurn_hand = info.getHand();

			long t = System.currentTimeMillis();
			Movement m = inTurn.getMovement(inTurn_hand, gameBoard);
			m = verifyMovement(m, info);
			t = (System.currentTimeMillis() - t)+1;
			info.getStatistics().addThinkingTime(t);
			if (m.pass)
				info.getStatistics().increasePassCount();
			informAll(m, inTurn);

			executeMovement(m, inTurn_hand);

			printMovement(m, info);
			printBoard();

			if (inTurn_hand.size() == 0) {
				return info;// vencedor
			}
		}
		return null;// não houve vencedor nesta rodada.
	}

	private void printBoard() {
		System.out.println("Board:");
		System.out.println(gameBoard);
		System.out.println();
	}

	private void printMovement(Movement m, AgentInfo a) {
		if (m.pass)
			System.out.println("player " + a.getName() + " has passed");
		else
			System.out.println("player " + a.getName() + " placed " + m.p);
	}

	private void executeMovement(Movement m, Hand inTurn_hand) {
		if (!m.pass) {
			inTurn_hand.removePiece(m.p);
			gameBoard.putPiece(m.p, m.side);
		}
	}

	private Movement verifyMovement(Movement m, AgentInfo inTurn) {
		if (!valid(m, inTurn)) {// se não for possivel o m1 ...
			m = inTurn.getAgent().wrongMovement(m); // pede um novo movimento
			if (!valid(m, inTurn)) {// se não for possivel novamente...
				m.pass = true;// obriga o jogador a passar a vez
			}
		}
		return m;
	}

	private boolean valid(Movement m, AgentInfo inTurn) {
		return m.pass
				|| (inTurn.getHand().HasPiece(m.p) && gameBoard.canPut(m.p));
	}

	private void informAll(Movement m, DominoAgent playerAgent) {
		for (AgentInfo aInfo : agents) {
			aInfo.getAgent().receiveInfo(m, playerAgent);
		}
	}

	private boolean gameIsOn() {
		for (AgentInfo agent : agents) {
			Hand h = agent.getHand();
			for (int i = 0; i < h.size(); i++) {
				if (gameBoard.canPut(h.getPiece(i))) {
					return true;
				}
			}
		}

		return false;
	}

	private void definePlayOrder() {
		int magicalRandomNumber = (int) (Math.random() * 10000);
		for (int i = 0; i < magicalRandomNumber; i++) {
			int rand = (((int) (Math.random() * 10000)) % (agents.size() - 1)) + 1;
			AgentInfo other = agents.remove(rand);
			AgentInfo first = agents.remove(0);
			agents.add(0, other);
			agents.add(rand, first);
		}
	}

	public void distributePieces() {
		DominoSet domSet = new DominoSet();
		while (domSet.remainingPieces() >= agents.size()) {
			for (AgentInfo aInfo : agents) {
				Hand h = aInfo.getHand();
				h.putPiece(domSet.drawRandom());
			}
		}
	}

	private void clearAll() {
		gameBoard = new Board();
		for (AgentInfo a : agents) {
			a.setHand(new Hand());
		}
	}

	private void prepare() {
		distributePieces();
		definePlayOrder();
		setFirstPlayer66();// quem começa o jogo é quem tem o dozão
	}

	private void setFirstPlayer66() {
		for (int i = 0; i < agents.size(); i++) {
			if (agents.get(i).getHand().HasPiece(new Piece(6, 6))) {
				AgentInfo first = agents.remove(i);
				agents.add(0, first);
				return;
			}
		}
	}

}
